Micron Document
🎖️GitЯра🎖️

Commit 4d9cf8ec18f79a0e2ce02df31e3f410b7942272c


Parents : fee0b35
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-08-19T23:42:36Z
Committer : GitHub <noreply@github.com>
Date : 2026-08-19T23:42:36Z

fix: derive temperature unit from locale temperature preference, not distance system (#6775)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Changes
Diff

diff --git a/core/common/src/androidHostTest/kotlin/org/meshtastic/core/common/util/TemperatureUnitTest.kt b/core/common/src/androidHostTest/kotlin/org/meshtastic/core/common/util/TemperatureUnitTest.kt
new file mode 100644
index 0000000000..2871a153d0
--- /dev/null
+++ b/core/common/src/androidHostTest/kotlin/org/meshtastic/core/common/util/TemperatureUnitTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.core.common.util
+
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import java.util.Locale
+import kotlin.test.assertEquals
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [34])
+class TemperatureUnitTest {
+
+ private lateinit var originalLocale: Locale
+
+ @Before
+ fun setUp() {
+ originalLocale = Locale.getDefault()
+ }
+
+ @After
+ fun tearDown() {
+ Locale.setDefault(originalLocale)
+ }
+
+ @Test
+ fun `US locale prefers Fahrenheit`() {
+ Locale.setDefault(Locale.US)
+ assertEquals(TemperatureUnit.FAHRENHEIT, getSystemTemperatureUnit())
+ }
+
+ // The UK mixes systems: miles for distance, Celsius for temperature. Temperature must not
+ // follow the (imperial) measurement system here.
+ @Test
+ fun `UK locale prefers Celsius despite imperial distances`() {
+ Locale.setDefault(Locale.UK)
+ assertEquals(TemperatureUnit.CELSIUS, getSystemTemperatureUnit())
+ assertEquals(MeasurementSystem.IMPERIAL, getSystemMeasurementSystem())
+ }
+
+ @Test
+ fun `German locale prefers Celsius`() {
+ Locale.setDefault(Locale.GERMANY)
+ assertEquals(TemperatureUnit.CELSIUS, getSystemTemperatureUnit())
+ }
+
+ // Android 14 regional preferences surface as the locale's "mu" Unicode extension.
+ @Test
+ fun `regional preference override wins over region default`() {
+ Locale.setDefault(Locale.forLanguageTag("en-US-u-mu-celsius"))
+ assertEquals(TemperatureUnit.CELSIUS, getSystemTemperatureUnit())
+ }
+}

diff --git a/core/common/src/androidMain/kotlin/org/meshtastic/core/common/util/LocaleUtils.android.kt b/core/common/src/androidMain/kotlin/org/meshtastic/core/common/util/LocaleUtils.android.kt
index 8dbf358b53..300623a1b7 100644
--- a/core/common/src/androidMain/kotlin/org/meshtastic/core/common/util/LocaleUtils.android.kt
+++ b/core/common/src/androidMain/kotlin/org/meshtastic/core/common/util/LocaleUtils.android.kt
@@ -19,6 +19,7 @@ package org.meshtastic.core.common.util
import android.icu.util.LocaleData
import android.icu.util.ULocale
import android.os.Build
+import androidx.core.text.util.LocalePreferences
import java.util.Locale
actual fun currentLocaleCode(): String = Locale.getDefault().language
@@ -52,3 +53,10 @@ actual fun getSystemMeasurementSystem(): MeasurementSystem {
}
}
}
+
+// LocalePreferences resolves from CLDR data and, on Android 14+, the user's Regional preferences
+// override. Kelvin (a valid regional preference) falls back to Celsius, which the app can display.
+actual fun getSystemTemperatureUnit(): TemperatureUnit = when (LocalePreferences.getTemperatureUnit()) {
+ LocalePreferences.TemperatureUnit.FAHRENHEIT -> TemperatureUnit.FAHRENHEIT
+ else -> TemperatureUnit.CELSIUS
+}

diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MeasurementSystem.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MeasurementSystem.kt
index 3ced2718a0..0244c21b5a 100644
--- a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MeasurementSystem.kt
+++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MeasurementSystem.kt
@@ -25,6 +25,22 @@ enum class MeasurementSystem {
/** returns the system's preferred measurement system. */
expect fun getSystemMeasurementSystem(): MeasurementSystem
+/**
+ * The system's preferred temperature unit. Deliberately decoupled from [MeasurementSystem]: some locales mix systems
+ * (the UK measures road distance in miles but temperature in Celsius), so temperature must never be derived from the
+ * distance unit.
+ */
+enum class TemperatureUnit {
+ CELSIUS,
+ FAHRENHEIT,
+}
+
+/**
+ * Returns the temperature unit preferred by the OS locale, honoring the user's regional-preference override where the
+ * platform supports one (Android 14+ Settings > System > Languages > Regional preferences).
+ */
+expect fun getSystemTemperatureUnit(): TemperatureUnit
+
/** Returns the device's current locale as a 2-letter ISO 639-1 language code (e.g. "en", "es", "fr"). */
expect fun currentLocaleCode(): String

diff --git a/core/common/src/iosMain/kotlin/org/meshtastic/core/common/util/NoopStubs.kt b/core/common/src/iosMain/kotlin/org/meshtastic/core/common/util/NoopStubs.kt
index e04222f8d1..0171a4f33e 100644
--- a/core/common/src/iosMain/kotlin/org/meshtastic/core/common/util/NoopStubs.kt
+++ b/core/common/src/iosMain/kotlin/org/meshtastic/core/common/util/NoopStubs.kt
@@ -40,6 +40,8 @@ actual object DateFormatter {
actual fun getSystemMeasurementSystem(): MeasurementSystem = MeasurementSystem.METRIC
+actual fun getSystemTemperatureUnit(): TemperatureUnit = TemperatureUnit.CELSIUS
+
actual fun currentLocaleCode(): String = "en"
actual fun currentRegionCode(): String = ""

diff --git a/core/common/src/jvmMain/kotlin/org/meshtastic/core/common/util/JvmPlatformUtils.kt b/core/common/src/jvmMain/kotlin/org/meshtastic/core/common/util/JvmPlatformUtils.kt
index 0661dfab1f..b61b2e8183 100644
--- a/core/common/src/jvmMain/kotlin/org/meshtastic/core/common/util/JvmPlatformUtils.kt
+++ b/core/common/src/jvmMain/kotlin/org/meshtastic/core/common/util/JvmPlatformUtils.kt
@@ -88,6 +88,20 @@ actual fun getSystemMeasurementSystem(): MeasurementSystem =
else -> MeasurementSystem.METRIC
}
+// CLDR unitPreferenceData lists these regions as defaulting to Fahrenheit; everywhere else is Celsius.
+actual fun getSystemTemperatureUnit(): TemperatureUnit =
+ when (Locale.getDefault().country.uppercase(Locale.getDefault())) {
+ "US",
+ "BS",
+ "BZ",
+ "KY",
+ "PR",
+ "PW",
+ -> TemperatureUnit.FAHRENHEIT
+
+ else -> TemperatureUnit.CELSIUS
+ }
+
actual fun currentLocaleCode(): String = Locale.getDefault().language
actual fun currentRegionCode(): String = Locale.getDefault().country

diff --git a/docs/en/developer/measurement.md b/docs/en/developer/measurement.md
index c959a27dc6..21a10bf81e 100644
--- a/docs/en/developer/measurement.md
+++ b/docs/en/developer/measurement.md
@@ -2,7 +2,7 @@
title: Measurement & Formatting
parent: Developer Guide
nav_order: 9
-last_updated: 2026-07-07
+last_updated: 2026-08-19
aliases:
- measurement
- metric-formatter
@@ -92,11 +92,13 @@ object NumberFormatter {
Three measurements convert away from metric for display, each gated by a boolean flag sourced from the user's device locale or preferences:
-| Measurement | Flag | Conversion |
-|---|---|---|
-| `temperature` | `isFahrenheit` | `°F = °C × 1.8 + 32` |
-| `windSpeed` | `isImperial` | m/s × 2.23694 → mph |
-| `rainfall` | `isImperial` | mm ÷ 25.4 → in |
+| Measurement | Flag | Source | Conversion |
+|---|---|---|---|
+| `temperature` | `isFahrenheit` | `getSystemTemperatureUnit()` | `°F = °C × 1.8 + 32` |
+| `windSpeed` | `isImperial` | `getSystemMeasurementSystem()` | m/s × 2.23694 → mph |
+| `rainfall` | `isImperial` | `getSystemMeasurementSystem()` | mm ÷ 25.4 → in |
+
+The two source functions (in `core/common/.../util/MeasurementSystem.kt`) are deliberately separate: some locales mix systems (the UK uses miles for distance but Celsius for temperature), so temperature must never be derived from the distance unit. On Android, `getSystemTemperatureUnit()` delegates to `androidx.core.text.util.LocalePreferences`, which resolves CLDR locale data and honors the Android 14+ Regional preferences temperature override.
Everything else (voltage, current, pressure, SNR, RSSI, humidity, percent) displays in its native metric units. The user-facing [Units & Locale](../user/units-and-locale) page explains what end users see.

diff --git a/docs/en/user/units-and-locale.md b/docs/en/user/units-and-locale.md
index 0a70963791..41cee07fb8 100644
--- a/docs/en/user/units-and-locale.md
+++ b/docs/en/user/units-and-locale.md
@@ -2,7 +2,7 @@
title: Units, Measurement & Locale
parent: User Guide
nav_order: 16
-last_updated: 2026-07-08
+last_updated: 2026-08-19
description: How the app formats temperature, distance, speed, and other measurements based on your device locale.
---
@@ -35,6 +35,8 @@ Temperature values from environment sensors are transmitted as **°C** and displ
This affects all temperature displays throughout the app: node environment telemetry, soil temperature, dew point, and telemetry chart axes.
+Temperature follows your locale's **temperature preference**, independent of the distance system. Locales that mix systems work correctly — a UK phone shows miles for distance but **°C** for temperature. On Android 14+, the **Temperature** regional preference (Settings → System → Languages → Regional preferences) overrides the locale default.
+
## Distance & Altitude
Distances between nodes and GPS altitudes are transmitted as **meters** and automatically scaled and converted.
@@ -113,7 +115,8 @@ On Android, your measurement system (metric vs imperial) is tied to your region
1. Open **Android Settings → System → Language & Region**
2. Change your **Region** or **Measurement units** preference
-3. Return to Meshtastic — values update immediately
+3. On Android 14+, temperature can be overridden on its own under **Regional preferences → Temperature**
+4. Return to Meshtastic — values update immediately
> 💡 **Tip:** All measurement formatting is handled centrally and respects your platform's locale, so units stay consistent everywhere in the app.

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt
index 25e87fc109..91a10e4c85 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.kt
@@ -25,6 +25,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
import org.koin.core.annotation.Single
+import org.meshtastic.core.common.util.TemperatureUnit
+import org.meshtastic.core.common.util.getSystemTemperatureUnit
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.DeviceLink
@@ -49,7 +51,6 @@ import org.meshtastic.feature.node.detail.NodeRequestActions
import org.meshtastic.feature.node.metrics.EnvironmentMetricsState
import org.meshtastic.feature.node.model.LogsType
import org.meshtastic.feature.node.model.MetricsState
-import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
import org.meshtastic.proto.DeviceProfile
import org.meshtastic.proto.FirmwareEdition
import org.meshtastic.proto.MeshPacket
@@ -200,7 +201,7 @@ constructor(
deviceLinks = deviceLinks,
reportedTarget = pioEnv,
isManaged = identity.profile.config?.security?.is_managed ?: false,
- isFahrenheit = displayUnits == DisplayUnits.IMPERIAL,
+ isFahrenheit = getSystemTemperatureUnit() == TemperatureUnit.FAHRENHEIT,
displayUnits = displayUnits,
deviceMetrics = logs.telemetry.filter { it.device_metrics != null },
localStats = logs.telemetry.filter { it.local_stats != null },

diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt
index 47aa3ba22e..15f840acf4 100644
--- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt
+++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.kt
@@ -28,8 +28,8 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
-import org.meshtastic.core.common.util.MeasurementSystem
-import org.meshtastic.core.common.util.getSystemMeasurementSystem
+import org.meshtastic.core.common.util.TemperatureUnit
+import org.meshtastic.core.common.util.getSystemTemperatureUnit
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeAddress
@@ -134,7 +134,7 @@ class NodeListViewModel(
// OS locale rarely changes mid-session; snapshot once instead of per filter/sort emission.
private val distanceUnits = DistanceUnit.getFromLocale().value
- private val tempInFahrenheit = getSystemMeasurementSystem() == MeasurementSystem.IMPERIAL
+ private val tempInFahrenheit = getSystemTemperatureUnit() == TemperatureUnit.FAHRENHEIT
val nodesUiState: StateFlow<NodesUiState> =
combine(nodeSortOption, nodeFilter) { sort, nodeFilter ->
NodesUiState(

Served by rngit 1.5.2 - Generated in 0.26s